/*
 * 
 * Copyright (C) 2010 Wilfried Elmenreich, Anita Sobe 
 * This file is part of VideoNetwork.
 * 
 * VideoNetwork is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 
 * You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>. 
 */
package proxy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;

import simulation.Simulator;
import simulation.StatKeeper;

import consumer.Consumer;
import consumer.Viewing;

public class ServerNode {

	private int id;

	private static int nextid = 0;

	private Simulator simulator;

	private Configuration config;

	private HormoneControls hCtrl;

	private ShortestPathRouting shortestPathRouting;

	private HashMap<Integer, Double> fetchTimeOut;

	private HashMap<Integer, Double> refTable, pheromoneTable;

	// stats can be public
	public StatKeeper storageStat = new StatKeeper();
	public StatKeeper delayStat = new StatKeeper();

	/**
	 * Units stored and available at this server
	 */
	private ArrayList<UnitContainer> storedUnits = new ArrayList<UnitContainer>();
	/**
	 * Units which are in the progress of a transfer operation
	 */
	private ArrayList<UnitContainer> incomingUnits = new ArrayList<UnitContainer>();

	/**
	 * outgoing units on all links
	 */
	private ArrayList<ArrayList<Migration>> outgoingUnits = new ArrayList<ArrayList<Migration>>();

	/**
	 * List of consumers directly connected to this server
	 */
	private ArrayList<Consumer> consumers = new ArrayList<Consumer>();

	/**
	 * 
	 */
	private ArrayList<Link> links = new ArrayList<Link>();

	/**
	 * 
	 * @param simulator
	 */
	public ServerNode(Simulator simulator) {
		this(simulator, null, null);
	}

	/**
	 * 
	 * @param simulator
	 * @param config
	 */
	public ServerNode(Simulator simulator, Configuration config) {
		this(simulator, config, null);
	}

	/**
	 * 
	 * @param simulator
	 * @param config
	 * @param hCtrl
	 */
	public ServerNode(Simulator simulator, Configuration config, HormoneControls hCtrl) {
		id = nextid++;
		this.simulator = simulator;
		this.config = config;
		// ensure at least default parameters!
		if (config == null)
			this.config = new Configuration();

		if (this.config.isFetchOnShortestRoute()) {
			this.shortestPathRouting = new ShortestPathRouting();
			this.fetchTimeOut = new HashMap<Integer, Double>();
		} else {

			this.refTable = new HashMap<Integer, Double>();
			this.pheromoneTable = new HashMap<Integer, Double>();
			this.hCtrl = hCtrl;

			if (hCtrl == null)
				this.hCtrl = new HormoneControls();
		}
	}

	/**
	 * Check if a given node is connected to current server
	 * 
	 * @param n
	 * @return
	 */
	public boolean containsTarget(ServerNode n) {
		for (Link li : links)
			if (li.getTargetNode() == n)
				return true;
		return false;
	}

	/**
	 * containedUnit checks if a unit is present on this server
	 * 
	 * @param id
	 *            of the unit
	 * @return if found, the respective UnitContainer object is returned,
	 *         otherwise a null pointer
	 */
	public UnitContainer searchPresentUnit(int id) {
		for (UnitContainer uc : storedUnits)
			if (uc.getVideoUnit().getId() == id)
				return uc;
		return null;
	}

	/**
	 * 
	 * @param id
	 * @return
	 */
	public UnitContainer searchInIncomingUnits(int id) {
		for (UnitContainer uc : incomingUnits)
			if (uc.getVideoUnit().getId() == id)
				return uc;
		return null;
	}

	/**
	 * 
	 * @param target
	 * @return
	 */
	public Link getLinkTo(ServerNode target) {
		for (Link li : links)
			if (li.getTargetNode() == target)
				return li;
		return null;
	}

	/**
	 * 
	 * @return
	 */
	public int diskUsedInBytes() {
		int used = 0;
		for (UnitContainer v : storedUnits)
			used += v.getVideoUnit().getSizeInBytes();
		return used;
	}

	/**
	 * 
	 * @return
	 */
	public int diskReservedinBytes() {
		int used = 0;
		for (UnitContainer v : incomingUnits)
			used += v.getVideoUnit().getSizeInBytes();
		return used;
	}

	/**
	 * 
	 * @return
	 */
	public int diskFreeinBytes() {
		return config.getDiskCapacity() - diskUsedInBytes() - diskReservedinBytes();
	}

	/**
	 * Gives a double value between 0 and 1 stating the quotient of used and
	 * reserved to available memory on this server
	 * 
	 * @return
	 */
	public double diskUsage() {
		return (double) (diskUsedInBytes() + diskReservedinBytes()) / config.getDiskCapacity();
	}

	/**
	 * 
	 * @param n
	 * @param datarate
	 * @param symmetric
	 */
	public void connectTo(ServerNode n, double datarate, boolean symmetric) {
		if (!this.containsTarget(n)) {
			Link li = new Link(n, datarate);
			links.add(li);
			outgoingUnits.add(new ArrayList<Migration>());
		}
		if (symmetric)
			if (!n.containsTarget(this)) {
				Link li = new Link(this, datarate);
				n.links.add(li);
				n.outgoingUnits.add(new ArrayList<Migration>());
			}
	}

	/**
	 * 
	 * @param id
	 * @param p
	 */
	public void increaseHormone(int id, double p) {
		double p0 = 0.0;
		// check if entry already exists
		if (pheromoneTable.containsKey(id)) {
			p0 = pheromoneTable.get(id);
		}
		pheromoneTable.put(id, p0 + p);
	}

	/**
	 * evaporate pheromone for all entries, remove entries of insignificant
	 * pheromone level
	 */
	public void evaporateHormones() {
		for (Iterator<Integer> iter = pheromoneTable.keySet().iterator(); iter.hasNext();) {
			int id = iter.next();
			double p0 = pheromoneTable.get(id);
			if (p0 - hCtrl.getEvaporationRate() * simulator.getSimStep() < hCtrl.getMinHormone())
				iter.remove(); // avoids ConcurrentModificationException
			else
				pheromoneTable.put(id, p0 - hCtrl.getEvaporationRate() * simulator.getSimStep());
		}
	}

	public void diffuseHormones() {
		// isolated node, we don't do anything!
		if (links.size() == 0)
			return;

		// walk through all pheromone types at this node
		for (int id : refTable.keySet()) {
			// spread to the neighbors
			double sumWeights = 0.0; // this is a heuristic for grading links
			double pheromoneToSpread = refTable.get(id) * hCtrl.getDiffusionAmount() * simulator.getSimStep();
			pheromoneTable.put(id, pheromoneTable.get(id) - pheromoneToSpread);
			// turn off diffuse if the regarding video unit is located here
			// rationale: in this case we do not need to attract another copy of
			// this unit
			if (searchPresentUnit(id) != null) {
				continue;
			}

			for (Link li : links) {
				double occupiedTime = li.getOccupiedUntil() - simulator.getSimTime();
				if (occupiedTime < 0)
					occupiedTime = 0;
				occupiedTime += VideoUnit.avgSize / li.getDatarate();
				sumWeights += 1.0 / occupiedTime;
			}

			for (Link li : links) {
				double occupiedTime = li.getOccupiedUntil() - simulator.getSimTime();
				if (occupiedTime < 0)
					occupiedTime = 0;
				occupiedTime += VideoUnit.avgSize / li.getDatarate();
				double myWeight = 1.0 / occupiedTime;
				li.getTargetNode().increaseHormone(id, pheromoneToSpread * myWeight / sumWeights);
			}

		}
	}

	void logUnitList() {
		String log = "S" + this.id + " has ";
		String delim = "";
		for (UnitContainer vc : this.storedUnits) {
			log += delim + vc.getVideoUnit().getId();
			delim = ",";
		}
		simulator.log(log);
	}

	public void migrateVideoUnits() {
		storageStat.add(diskUsage());
		// check for finished migrations
		// outgoing units
		for (Iterator<UnitContainer> iter = storedUnits.listIterator(); iter.hasNext();) {
			UnitContainer uc = iter.next();
			if ((uc.isDeleteAfterCopy()) && (uc.lastUse() < simulator.getSimTime())) {
				iter.remove();
			}
		}

		// incoming units
		for (Iterator<UnitContainer> iter = incomingUnits.listIterator(); iter.hasNext();) {
			UnitContainer uc = iter.next();
			if (simulator.getSimTime() >= uc.copyProcessUntil) {
				if (searchPresentUnit(uc.getVideoUnit().getId()) == null) {
					simulator.log("u" + uc.getVideoUnit().getId() + " has arrived at S" + id);
					storedUnits.add(uc); // unit has arrived
					// reset usage to current time
					uc.viewedUntil = simulator.getSimTime();
					uc.copyProcessUntil = simulator.getSimTime();
					logUnitList();
					iter.remove();

					// check if this video is to be routed
					if (uc.route != null) {
						simulator.log("reroute");
						shortestPathRouting.routeUnit(uc.route, uc.getVideoUnit().getId());
						uc.route = null; // the active route continues to live
						// in the incoming unit
					}
				}
			}
		}

		// handle stored units
		createPossibleMigrations();

	}

	private void createPossibleMigrations() {
		// check all units for transport
		for (UnitContainer uc : storedUnits) {
			// skip if unit is already being copied
			if (simulator.getSimTime() < uc.copyProcessUntil)
				continue;
			// get best link for transport
			Link bestlink = null;
			double highestPhero = 0;
			int id = uc.getVideoUnit().getId();
			for (Link li : links) {
				// get another link if this one is not usable
				if ((li.getOccupiedUntil() - simulator.getSimTime() > hCtrl.getBusyLinkThreshold()) || (li.getTargetNode().diskFreeinBytes() < uc.getVideoUnit().getSizeInBytes()))
					continue;
				if (li.getTargetNode().searchInIncomingUnits(uc.getVideoUnit().getId()) != null || li.getTargetNode().searchPresentUnit(uc.getVideoUnit().getId()) != null)
					continue;
				if (li.getTargetNode().pheromoneTable.containsKey(id)) {
					Double p = li.getTargetNode().pheromoneTable.get(id);
					if (p > highestPhero) {
						highestPhero = p;
						bestlink = li;
					}
				}
			}
			// didn't find a link
			if (bestlink == null)
				continue;

			// we do not have pheromones yet for the current unit!

			if (!pheromoneTable.containsKey(id))
				pheromoneTable.put(id, 0d);
			// worth to move/copy?
			if (highestPhero - pheromoneTable.get(id) > hCtrl.getMigrationThreshold()) {
				// check if unit is in use locally
				if (simulator.getSimTime() < uc.viewedUntil) {

					Migration m = new Migration(uc.getVideoUnit(), bestlink, true, highestPhero);
					if (!outgoingUnits.get(links.indexOf(bestlink)).contains(m)) {
						outgoingUnits.get(links.indexOf(bestlink)).add(m);
					}
				} else {// unit is not in use
					Migration m = new Migration(uc.getVideoUnit(), bestlink, false, highestPhero);
					if (!outgoingUnits.get(links.indexOf(bestlink)).contains(m)) {
						outgoingUnits.get(links.indexOf(bestlink)).add(m);
					}
				}
			}
		}
		// sort according to importance
		for (int i = 0; i < outgoingUnits.size(); i++) {
			Collections.sort(outgoingUnits.get(i), Collections.reverseOrder());
		}

		// try to send on all links
		for (int i = 0; i < outgoingUnits.size(); i++) {
			for (int j = 0; j < outgoingUnits.get(i).size(); j++) {
				Migration m = outgoingUnits.get(i).get(j);
				moveUnitTo(m.getVideoUnit(), m.getLink(), m.isCopy());
			}
		}

	}

	boolean moveUnitTo(VideoUnit vu, Link link, boolean copy) {

		if (links.indexOf(link) == -1)
			return false;
		ArrayList<Migration> ms = outgoingUnits.get(links.indexOf(link));
		for (int i = 0; i < ms.size(); i++) {
			if (ms.get(i).getVideoUnit().getId() == vu.getId()) {
				ms.remove(i);
			}
		}
		UnitContainer uc;

		uc = searchPresentUnit(vu.getId());

		if (uc == null)
			return false;

		if (link == null)
			return false;

		// failure if target server is not directly linked
		ServerNode target = link.getTargetNode();

		// check capacity at target server
		simulator.log("S" + this.id + ": trying to copy/move unit u" + vu.getId() + " to S" + target.getId() + " p: " + pheromoneTable.get(vu.getId()));

		// check if unit already present at target server
		if (target.searchPresentUnit(vu.getId()) != null) {
			// perhaps we can get rid of the unit
			simulator.log("u" + vu.getId() + " already present at S" + target.id);
			return false;
		}
		// check if this unit is already transfered to target server
		if (target.searchInIncomingUnits(vu.getId()) != null) {
			simulator.log("u" + vu.getId() + " already transfered to target S" + target.id);
			return false;
		}

		double freeCap = target.getDiskCapacity() - target.diskUsedInBytes() - target.diskReservedinBytes();
		if (freeCap < vu.getSizeInBytes()) {
			simulator.log("S" + this.id + ": not enough space at target S" + target.id + " freeCap: " + freeCap + " u:" + vu.getSizeInBytes());
			return false;
		}
		// set deleteAfterCopy if everything is fine
		uc.setDeleteAfterCopy(!copy);

		if (copy)
			simulator.migrationCopyCounter++;
		else
			simulator.migrationMoveCounter++;

		// link.addTransmission(uc, this, target, priority);

		if (simulator.getSimTime() > link.getOccupiedUntil())
			link.setOccupiedUntil(simulator.getSimTime());

		link.addOccupiedUntil(vu.getSizeInBytes() / link.getDatarate());
		// create new unitContainer for the target node
		target.incomingUnits.add(new UnitContainer(vu, link.getOccupiedUntil()));

		if (copy)
			simulator.log("S" + id + ": copy u" + vu.getId() + " to S" + target.id + " until " + link.getOccupiedUntil());
		else
			simulator.log("S" + id + ": move u" + vu.getId() + " to S" + target.id + " until " + link.getOccupiedUntil());
		return true;
	}

	private int getDiskCapacity() {
		return config.getDiskCapacity();
	}

	/**
	 * remove unused videos on the server in order to save disk space
	 */
	public void cleanUp() {
		if (config.isUseLRUcleanUp()) {
			LRUcleanUp();
			return;
		}

		if (diskUsage() < config.getDiskfullCleanupLimit())
			return;
		for (int i = 0; i < storedUnits.size(); i++) {
			UnitContainer uc = storedUnits.get(i);
			if (simulator.getSimTime() - uc.lastUse() > hCtrl.getCleanUpTriggerTime()) {
				if (getRidOfStoredUnit(uc.getVideoUnit().getId())) {
					i--;
				}
			}
		}

	}

	/**
	 * 
	 * @param id
	 * @return returns if unit could be deleted, otherwise unit will be moved
	 */
	boolean getRidOfStoredUnit(int id) {
		UnitContainer uc = searchPresentUnit(id);
		if (uc == null)
			return false;

		// try to delete if a another copy is available in the system
		if (countUnitOnNeighbors(uc.getVideoUnit().getId()) > 1) {
			// remove this unit
			storedUnits.remove(uc);
			simulator.log("S" + id + ": removed u" + uc.getVideoUnit().getId());
			logUnitList();
			return true; // was immediately deleted
		}

		// the unit cannot be deleted, but perhaps we can move it to another
		// server

		// select possible target node with respective pheromone
		Link bestlink = null;
		double highestPhero = Double.MIN_VALUE;
		for (Link li : links) {
			if ((li.getOccupiedUntil() - simulator.getSimTime() > hCtrl.getBusyLinkThreshold()) || (li.getTargetNode().diskFreeinBytes() < uc.getVideoUnit().getSizeInBytes()))
				continue;
			if (li.getTargetNode().searchInIncomingUnits(uc.getVideoUnit().getId()) != null || li.getTargetNode().searchPresentUnit(uc.getVideoUnit().getId()) != null)
				continue;
			if ((li.getTargetNode().pheromoneTable.containsKey(id)) && (li.getTargetNode().diskFreeinBytes() > uc.getVideoUnit().getSizeInBytes())) {
				Double p = li.getTargetNode().pheromoneTable.get(id);
				if (p > highestPhero) {
					highestPhero = p;
					bestlink = li;
				}
			}
		}
		if (bestlink != null) {

			Migration m = new Migration(uc.getVideoUnit(), bestlink, false, highestPhero);
			if (!outgoingUnits.get(links.indexOf(bestlink)).contains(m)) {
				outgoingUnits.get(links.indexOf(bestlink)).add(m);
				Collections.sort(outgoingUnits.get(links.indexOf(bestlink)));
				// send one to get rid of any unit
				Migration mC = outgoingUnits.get(links.indexOf(bestlink)).get(0);
				if (moveUnitTo(mC.getVideoUnit(), mC.getLink(), mC.isCopy()))
					simulator.log("S" + this.id + ": cleanup moving(phero) u" + mC.getVideoUnit().getId() + " to S" + bestlink.getTargetNode().id + ":");
				return false;
			}
		}
		// did not work, try to push this unit to a server with more free space
		bestlink = null;
		int myFreeSpace = diskFreeinBytes();
		int largestFreeSpace = myFreeSpace + uc.getVideoUnit().getSizeInBytes();
		for (Link li : links) {
			if ((li.getOccupiedUntil() - simulator.getSimTime() > hCtrl.getBusyLinkThreshold()) || (li.getTargetNode().diskFreeinBytes() < uc.getVideoUnit().getSizeInBytes()))
				continue;
			if (li.getTargetNode().searchInIncomingUnits(uc.getVideoUnit().getId()) != null || li.getTargetNode().searchPresentUnit(uc.getVideoUnit().getId()) != null)
				continue;
			int targetFreeSpace = li.getTargetNode().diskFreeinBytes();
			if (targetFreeSpace > largestFreeSpace) {
				largestFreeSpace = targetFreeSpace;
				bestlink = li;
			}
		}
		if (bestlink != null) {

			double importance = 0d;
			if (bestlink.getTargetNode().getPheromoneTable().containsKey(uc.getVideoUnit().getId())) {
				importance = bestlink.getTargetNode().getPheromoneTable().get(uc.getVideoUnit().getId());
			}

			Migration m1 = new Migration(uc.getVideoUnit(), bestlink, false, importance);

			if (!outgoingUnits.get(links.indexOf(bestlink)).contains(m1)) {
				outgoingUnits.get(links.indexOf(bestlink)).add(m1);
				Collections.sort(outgoingUnits.get(links.indexOf(bestlink)));
				Migration mC = outgoingUnits.get(links.indexOf(bestlink)).get(0);
				if (moveUnitTo(mC.getVideoUnit(), mC.getLink(), mC.isCopy()))
					simulator.log("S" + this.id + ": cleanup moving(diskcap) u" + mC.getVideoUnit().getId() + " to S" + bestlink.getTargetNode().id + ":");
				return false;
			}
		}
		return false;
	}

	/**
	 * 
	 * @param id
	 * @return
	 */
	private int countUnitOnNeighbors(int id) {
		int count = 0;
		for (Link li : links) {
			UnitContainer uc = li.getTargetNode().searchPresentUnit(id);
			if (uc != null) {
				if (!uc.isDeleteAfterCopy())
					count++;
			} else if (li.getTargetNode().searchInIncomingUnits(id) != null)
				count++;
		}
		return count;
	}

	/**
	 * 
	 * @param currentlyDemanding
	 * @param newRequest
	 */
	public void increaseInterest(ArrayList<Viewing> currentlyDemanding, boolean newRequest) {

		if (currentlyDemanding.size() == 0)
			return;

		if (config.isFetchOnShortestRoute()) {
			// sequential requests only next unit in sequence
			if (config.isPrioritizeTransmission()) {
				shortestPathRouting.fetchOnShortestRoute(simulator, this, currentlyDemanding.get(0).getVideoUnit().getId());
			} else {
				// all units in parallel
				for (Viewing vi : currentlyDemanding) {
					shortestPathRouting.fetchOnShortestRoute(simulator, this, vi.getVideoUnit().getId());
				}
			}
		} else {
			// hormone model
			double hormone;
			// handle different hormone amounts
			if (newRequest) {
				hormone = hCtrl.getHAmount1() * simulator.getSimStep();
			} else {
				hormone = hCtrl.getHAmount2() * simulator.getSimStep();
			}

			// all units at once
			for (Viewing vi : currentlyDemanding) {
				int uid = vi.getVideoUnit().getId();
				// increase hormones for all units not present
				if (this.searchPresentUnit(uid) == null) {

					// sequential request-> units near to playbacktime are
					// prioritized
					if (config.isPrioritizeTransmission()) {
						hormone *= (currentlyDemanding.size() - currentlyDemanding.indexOf(vi));
					}
					increaseHormone(uid, hormone);
				}
			}
		}

	}

	@SuppressWarnings("unchecked")
	private void LRUcleanUp() {
		if (diskUsage() < config.getDiskfullCleanupLimit())
			return;
		ArrayList<UnitContainer> localCopy = (ArrayList<UnitContainer>) storedUnits.clone();
		Collections.sort(localCopy); // sort according to last use from least
		// recently used to most recently used

		while (diskUsage() >= config.getDiskfullCleanupLimit()) {
			boolean deleteWorked = false;
			for (int i = 0; i < localCopy.size(); i++) {
				UnitContainer current = localCopy.get(i);
				if (countUnitOnNeighbors(current.getVideoUnit().getId()) > 1) {
					// remove this unit
					localCopy.remove(current);
					storedUnits.remove(current);
					simulator.log("S" + id + ": removed u" + current.getVideoUnit().getId());
					logUnitList();
					deleteWorked = true;
					break;
					// was immediately deleted
				}
			}
			if (deleteWorked == false)
				return; // we cannot delete any more units, they are all pinned
		}
	}

	// GETTERS AND SETTERS START HERE

	public ArrayList<UnitContainer> getStoredUnits() {
		return storedUnits;
	}

	public ArrayList<Link> getLinks() {
		return links;
	}

	public int getId() {
		return id;
	}

	public HashMap<Integer, Double> getPheromoneTable() {
		return pheromoneTable;
	}

	public ArrayList<Consumer> getConsumers() {
		return consumers;
	}

	public void setRefTable(HashMap<Integer, Double> refTable) {
		this.refTable = refTable;
	}

	public static void setNextId(int i) {
		nextid = i;
	}

	public HashMap<Integer, Double> getFetchTimeOut() {
		return fetchTimeOut;
	}

	public ArrayList<ArrayList<Migration>> getOutgoingUnits() {
		return outgoingUnits;
	}

}
